/*******************************************************************************
 * Copyright (c) 2000, 2008 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.search.internal.core.text;

import org.eclipse.core.filebuffers.FileBuffers;
import org.eclipse.core.filebuffers.ITextFileBuffer;
import org.eclipse.core.filebuffers.ITextFileBufferManager;
import org.eclipse.core.filebuffers.LocationKind;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.content.IContentDescription;
import org.eclipse.core.runtime.content.IContentType;
import org.eclipse.core.runtime.content.IContentTypeManager;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.text.IDocument;
import org.eclipse.search.core.text.TextSearchMatchAccess;
import org.eclipse.search.core.text.TextSearchRequestor;
import org.eclipse.search.core.text.TextSearchScope;
import org.eclipse.search.internal.core.text.FileCharSequenceProvider.FileCharSequenceException;
import org.eclipse.search.internal.ui.NewSearchUI;
import org.eclipse.search.internal.ui.SearchMessages;
import org.eclipse.search.internal.ui.SearchPlugin;

import java.io.CharConversionException;
import java.io.IOException;
import java.nio.charset.IllegalCharsetNameException;
import java.nio.charset.UnsupportedCharsetException;
import java.util.Collections;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * The visitor that does the actual work.
 */
public class TextSearchVisitor {

	public static class ReusableMatchAccess extends TextSearchMatchAccess {

		private int fOffset;
		private int fLength;
		private IFile fFile;
		private CharSequence fContent;

		public void initialize(IFile file, int offset, int length, CharSequence content) {
			fFile= file;
			fOffset= offset;
			fLength= length;
			fContent= content;
		}

		public IFile getFile() {
			return fFile;
		}

		public int getMatchOffset() {
			return fOffset;
		}

		public int getMatchLength() {
			return fLength;
		}

		public int getFileContentLength() {
			return fContent.length();
		}

		public char getFileContentChar(int offset) {
			return fContent.charAt(offset);
		}

		public String getFileContent(int offset, int length) {
			return fContent.subSequence(offset, offset + length).toString(); // must pass a copy!
		}
	}


	private final TextSearchRequestor fCollector;
	private final Matcher             fMatcher;

	private IProgressMonitor fProgressMonitor;

	private int   fNumberOfScannedFiles;
	private int   fNumberOfFilesToScan;
	private IFile fCurrentFile;

	private final MultiStatus fStatus;

	private final FileCharSequenceProvider fFileCharSequenceProvider;

	private final ReusableMatchAccess fMatchAccess;

	public TextSearchVisitor(TextSearchRequestor collector, Pattern searchPattern) {
		fCollector = collector;
		fStatus = new MultiStatus(NewSearchUI.PLUGIN_ID, IStatus.OK, SearchMessages.TextSearchEngine_statusMessage, null);

		fMatcher = searchPattern.pattern().length() == 0 ? null : searchPattern.matcher(new String());

		fFileCharSequenceProvider = new FileCharSequenceProvider();
		fMatchAccess = new ReusableMatchAccess();
	}

	public IStatus search(IFile[] files, IProgressMonitor monitor) {
		fProgressMonitor = monitor == null ? new NullProgressMonitor() : monitor;
		fNumberOfScannedFiles = 0;
		fNumberOfFilesToScan = files.length;
		fCurrentFile = null;

		Job monitorUpdateJob = new Job(SearchMessages.TextSearchVisitor_progress_updating_job) {
			private int fLastNumberOfScannedFiles = 0;

			public IStatus run(IProgressMonitor inner) {
				while (!inner.isCanceled()) {
					IFile file = fCurrentFile;
					if (file != null) {
						String fileName = file.getName();
						Object[] args = {fileName, new Integer(fNumberOfScannedFiles), new Integer(fNumberOfFilesToScan)};
						fProgressMonitor.subTask(Messages.format(SearchMessages.TextSearchVisitor_scanning, args));
						int steps = fNumberOfScannedFiles - fLastNumberOfScannedFiles;
						fProgressMonitor.worked(steps);
						fLastNumberOfScannedFiles += steps;
					}
					try {
						Thread.sleep(100);
					} catch (InterruptedException e) {
						return Status.OK_STATUS;
					}
				}
				return Status.OK_STATUS;
			}
		};

		try {
			String taskName = fMatcher == null ? SearchMessages.TextSearchVisitor_filesearch_task_label : Messages
					.format(SearchMessages.TextSearchVisitor_textsearch_task_label, fMatcher.pattern().pattern());
			fProgressMonitor.beginTask(taskName, fNumberOfFilesToScan);
			monitorUpdateJob.setSystem(true);
			monitorUpdateJob.schedule();
			try {
				fCollector.beginReporting();
				processFiles(files);
				return fStatus;
			} finally {
				monitorUpdateJob.cancel();
			}
		} finally {
			fProgressMonitor.done();
			fCollector.endReporting();
		}
	}

	public IStatus search(TextSearchScope scope, IProgressMonitor monitor) {
		return search(scope.evaluateFilesInScope(fStatus), monitor);
	}

	private void processFiles(IFile[] files) {
		final Map documentsInEditors;
//		if (PlatformUI.isWorkbenchRunning())
//			documentsInEditors = evalNonFileBufferDocuments();
//		else
			documentsInEditors = Collections.EMPTY_MAP;

		for (int i = 0; i < files.length; i++) {
			fCurrentFile = files[i];
			boolean res = processFile(fCurrentFile, documentsInEditors);
			if (!res)
				break;
		}
	}

	/**
//	 * @return returns a map from IFile to IDocument for all open, dirty editors
//	 */
//	private Map evalNonFileBufferDocuments() {
//		Map result = new HashMap();
//		IWorkbench workbench = SearchPlugin.getDefault().getWorkbench();
//		IWorkbenchWindow[] windows = workbench.getWorkbenchWindows();
//		for (int i = 0; i < windows.length; i++) {
//			IWorkbenchPage[] pages = windows[i].getPages();
//			for (int x = 0; x < pages.length; x++) {
//				IEditorReference[] editorRefs = pages[x].getEditorReferences();
//				for (int z = 0; z < editorRefs.length; z++) {
//					IEditorPart ep = editorRefs[z].getEditor(false);
//					if (ep instanceof ITextEditor && ep.isDirty()) { // only dirty editors
//						evaluateTextEditor(result, ep);
//					}
//				}
//			}
//		}
//		return result;
//	}

//	private void evaluateTextEditor(Map result, IEditorPart ep) {
//		IEditorInput input= ep.getEditorInput();
//		if (input instanceof IFileEditorInput) {
//			IFile file= ((IFileEditorInput) input).getFile();
//			if (!result.containsKey(file)) { // take the first editor found
//				ITextFileBufferManager bufferManager= FileBuffers.getTextFileBufferManager();
//				ITextFileBuffer textFileBuffer= bufferManager.getTextFileBuffer(file.getFullPath(), LocationKind.IFILE);
//				if (textFileBuffer != null) {
//					// file buffer has precedence
//					result.put(file, textFileBuffer.getDocument());
//				} else {
//					// use document provider
//					IDocument document= ((ITextEditor) ep).getDocumentProvider().getDocument(input);
//					if (document != null) {
//						result.put(file, document);
//					}
//				}
//			}
//		}
//	}

	public boolean processFile(IFile file, Map documentsInEditors) {
		try {
		    if (!fCollector.acceptFile(file) || fMatcher == null) {
		       return true;
		    }

			IDocument document= getOpenDocument(file, documentsInEditors);

			if (document != null) {
				DocumentCharSequence documentCharSequence= new DocumentCharSequence(document);
				// assume all documents are non-binary
				locateMatches(file, documentCharSequence);
			} else {
				CharSequence seq= null;
				try {
					seq= fFileCharSequenceProvider.newCharSequence(file);
					if (hasBinaryContent(seq, file) && !fCollector.reportBinaryFile(file)) {
						return true;
					}
					locateMatches(file, seq);
				} catch (FileCharSequenceProvider.FileCharSequenceException e) {
					e.throwWrappedException();
				} finally {
					if (seq != null) {
						try {
							fFileCharSequenceProvider.releaseCharSequence(seq);
						} catch (IOException e) {
							SearchPlugin.log(e);
						}
					}
				}
			}
		} catch (UnsupportedCharsetException e) {
			String[] args= { getCharSetName(file), file.getFullPath().makeRelative().toString()};
			String message= Messages.format(SearchMessages.TextSearchVisitor_unsupportedcharset, args);
			fStatus.add(new Status(IStatus.ERROR, NewSearchUI.PLUGIN_ID, IStatus.ERROR, message, e));
		} catch (IllegalCharsetNameException e) {
			String[] args= { getCharSetName(file), file.getFullPath().makeRelative().toString()};
			String message= Messages.format(SearchMessages.TextSearchVisitor_illegalcharset, args);
			fStatus.add(new Status(IStatus.ERROR, NewSearchUI.PLUGIN_ID, IStatus.ERROR, message, e));
		} catch (IOException e) {
			String[] args= { getExceptionMessage(e), file.getFullPath().makeRelative().toString()};
			String message= Messages.format(SearchMessages.TextSearchVisitor_error, args);
			fStatus.add(new Status(IStatus.ERROR, NewSearchUI.PLUGIN_ID, IStatus.ERROR, message, e));
		} catch (CoreException e) {
			String[] args= { getExceptionMessage(e), file.getFullPath().makeRelative().toString()};
			String message= Messages.format(SearchMessages.TextSearchVisitor_error, args);
			fStatus.add(new Status(IStatus.ERROR, NewSearchUI.PLUGIN_ID, IStatus.ERROR, message, e));
		} catch (StackOverflowError e) {
			String message= SearchMessages.TextSearchVisitor_patterntoocomplex0;
			fStatus.add(new Status(IStatus.ERROR, NewSearchUI.PLUGIN_ID, IStatus.ERROR, message, e));
			return false;
		} finally {
			fNumberOfScannedFiles++;
		}
		if (fProgressMonitor.isCanceled())
			throw new OperationCanceledException(SearchMessages.TextSearchVisitor_canceled);

		return true;
	}

	private boolean hasBinaryContent(CharSequence seq, IFile file) throws CoreException {
		IContentDescription desc= file.getContentDescription();
		if (desc != null) {
			IContentType contentType= desc.getContentType();
			if (contentType != null && contentType.isKindOf(Platform.getContentTypeManager().getContentType(IContentTypeManager.CT_TEXT))) {
				return false;
			}
		}

		// avoid calling seq.length() at it runs through the complete file,
		// thus it would do so for all binary files.
		try {
			int limit= FileCharSequenceProvider.BUFFER_SIZE;
			for (int i= 0; i < limit; i++) {
				if (seq.charAt(i) == '\0') {
					return true;
				}
			}
		} catch (IndexOutOfBoundsException e) {
		} catch (FileCharSequenceException ex) {
			if (ex.getCause() instanceof CharConversionException)
				return true;
			throw ex;
		}
		return false;
	}

	private void locateMatches(IFile file, CharSequence searchInput) throws CoreException {
		try {
			fMatcher.reset(searchInput);
			int k= 0;
			while (fMatcher.find()) {
				int start= fMatcher.start();
				int end= fMatcher.end();
				if (end != start) { // don't report 0-length matches
					fMatchAccess.initialize(file, start, end - start, searchInput);
					boolean res= fCollector.acceptPatternMatch(fMatchAccess);
					if (!res) {
						return; // no further reporting requested
					}
				}
				if (k++ == 20) {
					if (fProgressMonitor.isCanceled()) {
						throw new OperationCanceledException(SearchMessages.TextSearchVisitor_canceled);
					}
					k= 0;
				}
			}
		} finally {
			fMatchAccess.initialize(null, 0, 0, new String()); // clear references
		}
	}


	private String getExceptionMessage(Exception e) {
		String message= e.getLocalizedMessage();
		if (message == null) {
			return e.getClass().getName();
		}
		return message;
	}

	private IDocument getOpenDocument(IFile file, Map documentsInEditors) {
		IDocument document= (IDocument)documentsInEditors.get(file);
		if (document == null) {
			ITextFileBufferManager bufferManager= FileBuffers.getTextFileBufferManager();
			ITextFileBuffer textFileBuffer= bufferManager.getTextFileBuffer(file.getFullPath(), LocationKind.IFILE);
			if (textFileBuffer != null) {
				document= textFileBuffer.getDocument();
			}
		}
		return document;
	}

	private String getCharSetName(IFile file) {
		try {
			return file.getCharset();
		} catch (CoreException e) {
			return "unknown"; //$NON-NLS-1$
		}
	}

}

